package com.nth.android.radio;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;

import com.nth.android.Util;

import android.content.Context;
import android.media.MediaPlayer;
import android.os.Handler;
import android.util.Log;
import android.widget.Button;
import android.widget.TextView;

/**
 * MediaPlayer does not yet support streaming from external URLs so this class
 * provides a pseudo-streaming function by downloading the content incrementally
 * & playing as soon as we get enough audio in our temporary storage.
 * 
 * @author Igor Neuhold
 * 
 */
public class StreamingMediaPlayer {

    private static final int INTIAL_KB_BUFFER = 96 * 10 / 8;// assume
							    // 96kbps*10secs/8bits
							    // per byte

    TextView textStreamed;

    private Button playButton;
    
    private SimpleDateFormat sdf = new SimpleDateFormat("mm:ss");

    // Track for display by progressBar
    int totalKbRead = 0;

    // Create Handler to call View updates on the main UI thread.
    private final Handler handler = new Handler();

    MediaPlayer mediaPlayer;

    File downloadingMediaFile;

    private boolean isInterrupted;

    private Context context;

    private int counter = 0;

    /**
     * 
     * initialization method for Streaming media player,
     * 
     * @param context
     * @param textStreamed
     * @param playButton
     * @param streamButton
     */
    public StreamingMediaPlayer(Context context, TextView textStreamed, Button playButton, Button streamButton) {
	this.context = context;
	this.textStreamed = textStreamed;
	this.playButton = playButton;
    }

    /**
     * 
     * Progressivly download the media to a temporary location and update the
     * MediaPlayer as new content becomes available.
     * 
     * @param mediaUrl
     * @throws IOException
     */
    public void startStreaming(final String mediaUrl) throws IOException {
	Runnable r = new Runnable() {
	    public void run() {
		try {
		    downloadAudioIncrement(mediaUrl);
		} catch (IOException e) {
		    Log.e(getClass().getName(), "Unable to initialize the MediaPlayer for fileUrl=" + mediaUrl, e);
		    return;
		}
	    }
	};
	new Thread(r).start();
    }

    /**
     * 
     * Download the url stream to a temporary location and then call the
     * setDataSource for that local file
     * 
     * @param mediaUrl
     * @throws IOException
     */
    public void downloadAudioIncrement(String mediaUrl) throws IOException {

	URLConnection cn = new URL(mediaUrl).openConnection();
	cn.connect();
	InputStream stream = cn.getInputStream();
	if (stream == null) {
	    Log.e(getClass().getName(), "Unable to create InputStream for mediaUrl:" + mediaUrl);
	}

	downloadingMediaFile = new File(context.getCacheDir(), Util.getFileNameFromURL(mediaUrl));

	// Just in case a prior deletion failed because our code crashed or
	// something, we also delete any previously
	// downloaded file to ensure we start fresh. If you use this code,
	// always delete
	// no longer used downloads else you'll quickly fill up your hard disk
	// memory. Of course, you can also
	// store any previously downloaded file in a separate data cache for
	// instant replay if you wanted as well.
	if (downloadingMediaFile.exists()) {
	    downloadingMediaFile.delete();
	}

	FileOutputStream out = new FileOutputStream(downloadingMediaFile);
	byte buf[] = new byte[16384];
	int totalBytesRead = 0, incrementalBytesRead = 0;
	do {
	    int numread = stream.read(buf);
	    if (numread <= 0)
		break;
	    out.write(buf, 0, numread);
	    totalBytesRead += numread;
	    incrementalBytesRead += numread;
	    totalKbRead = totalBytesRead / 1000;

	    testMediaBuffer();
	} while (validateNotInterrupted());
	stream.close();
	if (validateNotInterrupted()) {
	    fireDataFullyLoaded();
	}
    }

    /**
     * 
     * check if stream has been interrupted
     * 
     * @return boolean
     */
    private boolean validateNotInterrupted() {
	if (isInterrupted) {
	    if (mediaPlayer != null) {
		mediaPlayer.pause();
		// mediaPlayer.release();
	    }
	    return false;
	}
	return true;
    }

    /**
     * Test whether we need to transfer buffered data to the MediaPlayer.
     * Interacting with MediaPlayer on non-main UI thread can causes crashes to
     * so perform this using a Handler.
     */
    private void testMediaBuffer() {
	Runnable updater = new Runnable() {
	    public void run() {
		if (mediaPlayer == null) {
		    // Only create the MediaPlayer once we have the minimum
		    // buffered data
		    if (totalKbRead >= INTIAL_KB_BUFFER) {
			try {
			    startMediaPlayer();
			} catch (Exception e) {
			    Log.e(getClass().getName(), "Error copying buffered conent.", e);
			}
		    }
		} else if (mediaPlayer.getDuration() - mediaPlayer.getCurrentPosition() <= 1000) {
		    // NOTE: The media player has stopped at the end so transfer
		    // any existing buffered data
		    // We test for < 1second of data because the media player
		    // can stop when there is still
		    // a few milliseconds of data left to play
		    transferBufferToMediaPlayer();
		}
	    }
	};
	handler.post(updater);
    }

    /**
     * Starts media player, when enough data is in buffer play button is
     * activated
     */
    void startMediaPlayer() {
	try {
	    File bufferedFile = new File(context.getCacheDir(), "playingMedia" + (counter++) + ".dat");

	    // We double buffer the data to avoid potential read/write errors
	    // that could happen if the
	    // download thread attempted to write at the same time the
	    // MediaPlayer was trying to read.
	    // For example, we can't guarantee that the MediaPlayer won't open a
	    // file for playing and leave it locked while
	    // the media is playing. This would permanently deadlock the file
	    // download. To avoid such a deadloack,
	    // we move the currently loaded data to a temporary buffer file that
	    // we start playing while the remaining
	    // data downloads.
	    moveFile(downloadingMediaFile, bufferedFile);

	    Log.e(getClass().getName(), "Buffered File path: " + bufferedFile.getAbsolutePath());
	    Log.e(getClass().getName(), "Buffered File length: " + bufferedFile.length() + "");

	    mediaPlayer = createMediaPlayer(bufferedFile);

	    // We have pre-loaded enough content and started the MediaPlayer so
	    // update the buttons & progress meters.
	    mediaPlayer.start();
	    startPlayProgressUpdater();
	    if (playButton != null) {
	    playButton.setEnabled(true);
	    }
	} catch (IOException e) {
	    Log.e(getClass().getName(), "Error initializing the MediaPlayer.", e);
	    return;
	}
    }

    /**
     * 
     * Creates media player which plays buffered file
     * 
     * @param mediaFile
     * @return MediaPlayer
     * @throws IOException
     */
    private MediaPlayer createMediaPlayer(File mediaFile) throws IOException {
	MediaPlayer mPlayer = new MediaPlayer();
	mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
	    public boolean onError(MediaPlayer mp, int what, int extra) {
		Log.e(getClass().getName(), "Error in MediaPlayer: (" + what + ") with extra (" + extra + ")");
		return false;
	    }
	});

	// It appears that for security/permission reasons, it is better to pass
	// a FileDescriptor rather than a direct path to the File.
	// Also I have seen errors such as "PVMFErrNotSupported" and
	// "Prepare failed.: status=0x1" if a file path String is passed to
	// setDataSource(). So unless otherwise noted, we use a FileDescriptor
	// here.
	FileInputStream fis = new FileInputStream(mediaFile);
	mPlayer.setDataSource(fis.getFD());
	mPlayer.prepare();
	return mPlayer;
    }

    /**
     * Transfer buffered data to the MediaPlayer. NOTE: Interacting with a
     * MediaPlayer on a non-main UI thread can cause thread-lock and crashes so
     * this method should always be called using a Handler.
     */
    void transferBufferToMediaPlayer() {
	try {
	    // First determine if we need to restart the player after
	    // transferring data...e.g. perhaps the user pressed pause
	    boolean wasPlaying = mediaPlayer.isPlaying();
	    int curPosition = mediaPlayer.getCurrentPosition();

	    // Copy the currently downloaded content to a new buffered File.
	    // Store the old File for deleting later.
	    File oldBufferedFile = new File(context.getCacheDir(), "playingMedia" + counter + ".dat");
	    File bufferedFile = new File(context.getCacheDir(), "playingMedia" + (counter++) + ".dat");

	    // This may be the last buffered File so ask that it be delete on
	    // exit. If it's already deleted, then this won't mean anything. If
	    // you want to
	    // keep and track fully downloaded files for later use, write
	    // caching code and please send me a copy.
	    bufferedFile.deleteOnExit();
	    moveFile(downloadingMediaFile, bufferedFile);

	    // Pause the current player now as we are about to create and start
	    // a new one. So far (Android v1.5),
	    // this always happens so quickly that the user never realized we've
	    // stopped the player and started a new one
	    mediaPlayer.pause();

	    // Create a new MediaPlayer rather than try to re-prepare the prior
	    // one.
	    mediaPlayer = createMediaPlayer(bufferedFile);
	    mediaPlayer.seekTo(curPosition);

	    // Restart if at end of prior buffered content or mediaPlayer was
	    // previously playing.
	    // NOTE: We test for < 1second of data because the media player can
	    // stop when there is still
	    // a few milliseconds of data left to play
	    boolean atEndOfFile = mediaPlayer.getDuration() - mediaPlayer.getCurrentPosition() <= 1000;
	    if (wasPlaying || atEndOfFile) {
		mediaPlayer.start();
	    }

	    // Lastly delete the previously playing buffered File as it's no
	    // longer needed.
	    oldBufferedFile.delete();

	} catch (Exception e) {
	    Log.e(getClass().getName(), "Error updating to newly loaded content.", e);
	}
    }

    /**
     * 
     * transfers buffer to media player and then deletes unnecessary file
     */
    private void fireDataFullyLoaded() {
	Runnable updater = new Runnable() {
	    public void run() {
		transferBufferToMediaPlayer();

		// Delete the downloaded File as it's now been transferred to
		// the currently playing buffer file.
		downloadingMediaFile.delete();
		//textStreamed.setText(("Audio full loaded: " + totalKbRead + " Kb read"));
	    }
	};
	handler.post(updater);
    }

    /**
     * 
     * getter method for mediaPlayer
     * 
     * @return MediaPlayer
     */
    public MediaPlayer getMediaPlayer() {
	return mediaPlayer;
    }

    /**
     * 
     * update function for progress state
     */
    public void startPlayProgressUpdater() {
	if (mediaPlayer.isPlaying()) {
	    Runnable notification = new Runnable() {
		public void run() {
		    startPlayProgressUpdater();
		}
	    };
	    handler.postDelayed(notification, 1000);
	}
    }

    /**
     * 
     * interrupts media player, if it not null, media player will pause
     */
    public void interrupt() {
	if (playButton != null) {
	    playButton.setEnabled(false);
	    }
	isInterrupted = true;
	validateNotInterrupted();
    }

    /**
     * Move the file in oldLocation to newLocation.
     */
    public void moveFile(File oldLocation, File newLocation) throws IOException {

	if (oldLocation.exists()) {
	    BufferedInputStream reader = new BufferedInputStream(new FileInputStream(oldLocation));
	    BufferedOutputStream writer = new BufferedOutputStream(new FileOutputStream(newLocation, false));
	    try {
		byte[] buff = new byte[8192];
		int numChars;
		while ((numChars = reader.read(buff, 0, buff.length)) != -1) {
		    writer.write(buff, 0, numChars);
		}
	    } catch (IOException ex) {
		throw new IOException("IOException when transferring " + oldLocation.getPath() + " to " + newLocation.getPath());
	    } finally {
		try {
		    if (reader != null) {
			writer.close();
			reader.close();
		    }
		} catch (IOException ex) {
		    Log.e(getClass().getName(), "Error closing files when transferring " + oldLocation.getPath() + " to " + newLocation.getPath());
		}
	    }
	} else {
	    throw new IOException("Old location does not exist when transferring " + oldLocation.getPath() + " to " + newLocation.getPath());
	}
    }
}